[Prihodnji teden verjetno ne bom imel dostopa do e-maila, zato ze kar zdajle
posiljam tele blodnje o najkrajsih poteh v grafih.  Zdi se mi, da je zdajle
primeren cas za Dijkstrov algoritem, ker smo se ravno ukvarjali s kopicami,
Bellman-Ford in Floyd-Warshall pa sta po svoje sicer mogoce manj pogosto
uporabna na raznih tekmovanjih, se mi pa zdita zanimiva tudi kot primera
dinamicnega programiranja.  Na koncu razdelka o Dijkstrovem algoritmu in na
koncu celega maila so tudi linki na nekaj nalog, pri katerih pridejo ti
algoritmi prav.]

--

Dijkstrov algoritem za najkrajse poti v grafu

Doslej smo videli ze dva nacina, kako odkriti najkrajse poti v grafu:
ce je graf aciklicen, lahko uporabimo topolosko urejanje; ce pa za
dolzino poti vzamemo le stevilo povezav v njej (kar je isto, kot ce
bi rekli, da so vse povezave enako dolge), lahko uporabimo iskanje
v sirino.  Obstajajo pa se splosnejsi algoritmi za iskanje najkrajsih
poti.  Eden od njih je Dijkstrov algoritem (Edsgar Dijkstra), ki je
primeren za poljubne grafe, vazno je le, da nobena povezava nima
negativne dolzine.  Drugace pa ima lahko graf tudi cikle, lahko so
povezave razlicno dolge in tako naprej.

Problem, ki ga hocemo resiti, je torej taksen: imamo nek graf in v
njem neko zacetno tocko, recimo s.  Zanimajo nas najkrajse poti od s
do vseh ostalih tock.  Oznacimo z d(u) dolzino najkrajse poti od
s do tocke u, z d(u, v) pa dolzino povezave od u do v (ce take
povezave ni, si lahko zaenkrat mislimo, da je d(u, v) = neskoncno).

[Pozor: ceprav sem se zelo potrudil, da bi bil naslednji opis
Dijkstrovega algoritma zelo natancen in jasen, je seveda mogoce,
da vi ne boste tega mnenja ali pa se vam bo zdel opis malo puscoben.
Preden zacnete zabadati igle v vudu lutko z mojo podobo, vam
priporocam, da si ogledate kaksno od nazornih animacij Dijkstrovega
algoritma, npr.
http://www.cs.uwa.edu.au/undergraduate/courses/230.300/readings/graphapplet/
dijkstra.html
Koristna je tudi tale stran:
http://ciips.ee.uwa.edu.au/~morris/Year2/PLDS210/dijkstra.html]

Opirali se bomo na dejstvo, da je najkrajsa pot od s do v vedno
podaljsek drugih najkrajsih poti.  Na primer: recimo, da najkrajsa
pot od s do v nekje vmes obisce tudi neko tocko u.  Potem trdimo,
da je ta njen potek od s do u tudi najkrajsa pot od s do u.  (Dokaz:
ce bi obstajala kaksna se krajsa pot od s do u, bi lahko to pot
potem nadaljevali do v-ja na enak nacin kot pri nasi zgoraj omenjeni
poti, s tem pa bi dobili neko pot od s do v, ki bi bila krajsa od
tiste nase prej omenjene, za tisto pa smo predpostavili, da je
najkrajsa, torej bi prisli v protislovje.)

Pri Dijkstrovem algoritmu so tocke grafa ves cas razdeljene na tri
skupine: (A) tiste, do katerih ze poznamo najkrajso pot (od s);
(B) tiste, do katerih ze poznamo neko pot, ki pa se ni nujno
najkrajsa; (C) tiste, do katerih se ne poznamo nobene poti.

Ves cas bomo vzdrzevali naslednjo lastnost (recimo ji X): v B
so natanko vse tiste tocke, ki niso v A, so pa dosegljive z enim
korakom iz kaksne tocke iz A; in nadalje za vsako tocko u iz B
poznamo najkrajso izmed vseh tistih poti, ki potekajo ves cas
skozi tocke iz A, le v zadnjem koraku stopijo v B.  Naj bo
za posamezni u iz B dolzina te najkrajse izmed teh poti enaka
d'(u).  [Ocitno je d'(u) <= d(u).]

Zacetno stanje bo taksno: v mnozico A postavimo tocko s, v mnozico
B njene sosede, v mnozico C pa vse ostale tocke.  Za vsako od
s-jevih sosed, recimo u, poznamo zelo preprosto pot "s->u" z
dolzino d(s, u).  Ocitno ta razdelitev tock ustreza pogoju X:
ker je v A le tocka s, morajo biti v B le njene sosede, saj so le
te dosegljive iz s v enem koraku; najkrajsa pot, ki gre skozi
tocke iz A in na koncu v taksno sosedo, pa je seveda kar oblike
s->u, saj je s trenutno edina tocka iz A.

Zdaj pa trdimo naslednje: naj bo izmed vseh tock v mnozici B
tocka "v" tista, ki ima najmanjso vrednost d'(v).  Potem trdimo,
da je d'(v) = d(v).  Z besedami: najkrajsa taka pot, ki se zacne
pri s, gre nekaj casa po A, nato pa v zadnjem koraku stopi v
tocko "v", je dejansko najkrajsa izmed vseh poti od s do v.

O tem se lahko prepricamo takole: recimo, da obstaja neka se
krajsa pot od s do v.  Ker je s v mnozici A, bo tudi ta pot
nekaj casa se hodila po mnozici A, nato pa bo naredila en
korak ven, recimo v neko tocko u, ki ni iz A.  Ker velja za
naso trenutno razdelitev tock na mnozice A, B in C lastnost X,
mora biti tocka u iz mnozice B.  Ta doslej prehojeni del
poti mora biti cim krajsi, da bo tudi celotna pot od s do v
najkrajsa.  Najkrajsa pot od s skozi A in v zadnjem koraku do u
pa je dolga, spet zaradi lastnosti X, ravno d'(u).  Nato se
ta nasa domnevna najkrajsa pot nadaljuje od u do v, zato
mora biti d(v) = d'(u) + [dolzina preostanka poti, torej od
u do v] >= d'(u).  To pa je naprej >= d'(v), saj smo rekli,
naj bo v izmed vseh tock iz B tista z najmanjso d'(v), torej
mora biti d'(v) <= d'(u).  Tako smo videli: d(v) >= d'(v).
Po drugi strani pa je jasno (kot smo ugotovili ze zgoraj),
da je d'(v) dolzina _neke_ poti od s do v in je zato gotovo
vsaj tako dolga kot najkrajsa pot, zato je d'(v) >= d(v).
Ce zdaj to dvoje zdruzimo, je d'(v) = d(v).  Prav to pa smo
hoteli dokazati.

In zdaj, ko to vemo, se splaca narediti naslednje: do tocke "v"
torej dejansko ze poznamo pravo najkrajso pot, zato jo je
pametno prestaviti v mnozico A.  Nato pa je treba, da bomo
ohranili veljavnost lastnosti X, prestaviti v mnozico B tiste
tocke, ki so v-jeve sosede, pa so bile doslej se v mnozici C.
Nadalje je tudi treba upostevati pri ocenah d'(u) za tocke
u iz B moznost, da gre pot od s skozi A in na koncu do u
zdaj po novem tudi skozi tocko v.  No, ocitno nima smisla, da
bi sla taka pot skozi v in potem se nekam naprej po mnozici A.
[Recimo namrec, da bi prisla do v, nato od tam v enem koraku
do nekega w iz A in potem po se enem ali vec korakih koncno
do u.  Potem lahko do tega w gotovo pridemo tudi po kaksni enako
kratki poti, ki ne gre skozi v, saj ce ne bi bilo tako, bi sla
najkrajsa pot do w-ja skozi v, to pa je nemogoce, saj smo v
dodali v A sele v zadnjem koraku, torej je moral biti w tam
ze od prej, torej smo ze prej poznali pravo najkrajso pot do
njega, torej ta pot ni sla skozi v.]  Torej je lahko taka
pot le taksne oblike: zacne v s, nekaj casa se sprehaja po
tockah iz A, nazadnje obisce v in od tam v enem koraku stopi
v tocko u.  Torej je vrednost d(v) + d(v, u) nova kandidatka za
dolzino najkrajse poti iz s skozi A in v zadnjem koraku do u;
ce je dosedanja d'(u) vecja od tega, postavimo d'(u) := d(v) +
d(v, u) in si zapomnimo, da trenutno najobetavnejsa pot do
u-ja v zadnjem koraku pride skozi v.

Ko tako spet vzpostavimo lastnost X, lahko spet naredimo enak
korak.  Vzamemo torej iz B tisto tocko u, ki ima najmanjso
d'(u), in zdaj vemo, da je d(u) = d'(u), dodamo v B kaj novih
tock iz C (ce je treba), osvezimo vrednosti d' za tocke iz B
(ce je treba) in tako naprej.  Ta postopek se konca, ko se
mnozica B izprazni.  Takrat imamo v A vse tocke, dosegljive iz
s, zanje pa tudi poznamo najkrajse poti.  V C so le se tocke,
do katerih iz s sploh ne moremo priti.

Abstraktno obliko Dijkstrovega postopka lahko zdaj zapisemo
takole:

(*  V = mnozica tock; E = mnozica povezav;
    d[u, v] = dolzina povezave od u do v;
    d[u] = dolzina doslej najkrajse znane poti od s do u;
    p[u] = prednik u-ja na tej najkrajsi poti *)
 1  A := {s}; B := {}; C := {};
 2  za vsako tocko u iz V:
 3    ce obstaja v E povezava (s->u):
 4      p[u] := s;
 5      d[u] := d[s, u];
 6      dodaj u v mnozico B;
 7    sicer:
 8      p[u] := nil;
 9      dodaj u v mnozico C;
10  dokler B ni prazna:
11    naj bo u med tockami iz B tista z najmanjso d[u];
12    zbrisi u iz mnozice B;
13    dodaj u v mnozico A;
14    za vsako u-jevo naslednico, recimo "v":
15      ce je "v" v mnozici B:
16        ce je d[u] + d[u, v] < d[v]:
17          d[v] := d[u] + d[u, v];
18          p[v] := u;
19      sicer, ce je "v" v mnozici C:
20        d[v] := d[u] + d[u, v];
21        p[v] := u;
22        zbrisi "v" iz mnozice C;
23        dodaj "v" v mnozico B;

Ko se ta algoritem konca, lahko iz vsake tocke u sledimo
kazalcem na predhodnike p[u], p[p[u]], p[p[p[u]]] itd.
in scasoma pridemo do s.  Tako lahko od zadnjega konca proti
zacetku izsledimo najkrajso pot od s do u.  Obenem imamo v
d[u] tudi dolzino te najkrajse poti.

[Animacija na prvem od zgoraj navedenih URLjev ima
tocko s narisano modro, ostale tocke iz mnozice A rumeno,
tocke iz mnozice B rdece in tocke iz mnozice C sivo.]

Zdaj pa razmislimo o bolj konkretni implementaciji.  Za tocke
iz mnozice B moramo biti sposobni ucinkovito ugotoviti, katera
ima najmanjso vrednost d[u].  Tocke se v mnozico B kar naprej
dodaja in brise, lahko pa se tudi kaksni tocki, ki je ze v
mnozici B, vrednost d[u] spremeni (zmanjsa, ce smo natancni)
(vrstici 17 in 18).  Ocitno je za taksne operacije ravno
prava podatkovna struktura kopica -- natancneje, potrebujemo
taksno kopico, ki ima pri vrhu manjse vrednosti d[u], tako da
bomo imeli v korenu vedno pri roki najmanjso vrednost po celi
mnozici B.  Bo pa zaradi vrstice 17 potrebno v kopici ob vsaki
vrednosti d[u] hraniti tudi pripadajoci u, zato da bomo lahko
ob premikanju elementov po kopici vzdrzevali tudi nekaksno
kazalo, v katerem bo pisalo, kje v kopici (na katerem indeksu)
se posamezna tocka trenutno nahaja.  To je potrebno, zato da
bo vrstica 17 lahko vedela, kje v kopici je vrednost d[u], ki
jo mora spremeniti.  Imeli bomo torej neko tabelo, kjer bo
za vsako tocko pisalo, kje v kopici se nahaja.  Isto tabelo
bomo lahko uporabili tudi, da bomo za tocke iz mnozic A oz.
C zapisali, da so pac v A oz. C, ne pa v kopici (in s tem v
mnozici B).

const A = -1; C = -2;
var p, Kje: array[1..n] of Integer;
    ShPath: array[1..n] of Double; (* najkrajsa pot *)
    u, v: Integer;
type THeapRec = record u: Integer; d: Double; end;
var H: array[0..n-1] of THeapRec; (* kopica *)
    B: Integer; (* stevilo tock v kopici *)

    (* Mislimo si, da je v H[i] trenutno element u z vrednostjo v.
       Ta podprogram ga bo po potrebi premaknil vise in ustrezno
       popravljal vsebino tabele Kje. *)
    procedure Lift(i, u: Integer; d: Double);
    var pi: Integer;
    begin
      while i > 0 do begin
        pi := (i - 1) div 2;
        if H[pi].d <= d then Break;
        H[i] := H[pi]; Kje[H[i].u] := i;
        i := pi; end;
      Kje[u] := i; H[i].u := u; H[i].d := d;
    end;

    (* Doda u na konec in poklice Lift. *)
    procedure Add(u: Integer; d: Double);
      begin B := B + 1; Lift(B-1, u, d); ShPath[u] := d; end;

    (* Vrne vrednost iz korena kopice in jo tudi
       zbrise iz kopice.  Sledi seveda pogrezanje
       bivsega zadnjega elementa. *)
    function DeleteRoot: Integer;
    var i, ci, u: Integer; d: Double;
    begin
      DeleteRoot := H[0].u;
      B := B - 1; u := H[B].u; d := H[B].d; i := 0;
      while 2*i + 1 < B do begin
        ci := 2*i + 1;
        if ci+1 < B then if H[ci+1].d < H[ci].d then ci := ci+1;
        if d < H[ci] then Break;
        H[i] := H[ci]; Kje[H[i].u] := i;
        i := ci; end;
      H[i] := H[B]; Kje[H[i].u] := i;
    end;

begin
  (* inicializacija *)
  ShPath[s] := 0; B := 0;
  (* dodajmo s-jeve naslednike v kopico, ostale pa v tocko C *)
  for u := 1 to n do
    if obstaja povezava (s->u) then
      begin Add(u, EdgeLen[s, u]); P[u] := s; end
    else
      begin P[u] := -1; Kje[u] := C; end;
  (* ponavljajmo, dokler kopica ni prazna *)
  while B > 0 do
    begin
    (* vemo, da je ShPath[u] najkrajsa pot do u-ja,
       p[u] pa u-jev predhodnik na tej poti *)
    u := DeleteRoot; Kje[u] := A;
    for v := [po vseh u-jevih naslednikih] do
      if Kje[v] = C then
        (* prvic smo odkrili pot do v-ja *)
        begin Add(v, ShPath[u] + EdgeLen[s, u]); P[v] := u; end
      else if Kje[v] = B then
        if ShPath[u] + EdgeLen[s, u] < ShPath[v] then begin
          (* odkrili smo boljso pot do v-ja od doslej
             najboljse znane *)
          ShPath[v] := ShPath[u] + EdgeLen[s, u]; P[v] := u;
          Lift(Kje[i], v, ShPath[v]); end;
    end;
  (* tako, zdaj imamo za vse tocke u, za katere je Kje[u] = A,
     poznane dolzine najkrajse poti od s do u in u-jevega
     predhodnika na tej poti *)
end;

Se ena opomba: tu smo ves cas govorili o s-jevih in kasneje
u-jevih "naslednikih", kot da je graf usmerjen.  Ce ni
usmerjen, moramo pac gledati vse sosede, vse ostalo pa
ostane nespremenjeno.

Kaksna pa je casovna zahtevnost tega algoritma?
1 Vsako tocko enkrat dodamo v kopico (ko jo premaknemo iz
  C v B) in enkrat vzamemo ven (ko jo premaknemo iz B v A)
  [no, ne nujno _vsako_ tocko -- le tiste tocke, ki so
  dosegljive iz s; ampak v najslabsem primeru so to pac kar
  vse tocke grafa].  Add in DeleteRoot se torej kliceta
  najvec n-krat (ce imamo n tock v grafu).
2 Ko vzamemo neko tocko iz kopice, moramo pogledati njene
  naslednice in v najslabsem primeru za vsako klicemo Lift.
  Lift se torej klice se najvec |E|-krat (ce je |E| stevilo
  povezav v grafu).
3 Vsaka od operacij s kopico (Add, Lift, DeleteRoot) traja
  O(globina kopice) casa, globina kopice pa je
  O(log(stevilo elementov v kopici)), kar je v najslabsem
  primeru O(log n) (ce bi bile v kopici skoraj vse tocke).
4 Ostale operacije v glavnem bloku begin..end so
  izvedljive v konstantnem casu oz. nad vsem skupaj
  asimptoticno gledano prevladujejo operacije nad kopico.
Skupaj imamo O(n) operacij nad kopico pod tocko (1),
O(|E|) operacij nad kopico pod tocko (2), vsaka od teh pa
stane O(log n) casa (tocka 3), torej imamo skupaj
O((n + |E|) log n) dela za graf z n tockami in |E| povezavami.
  Glede tocke 4 pa moramo poudariti, da smo predpostavili,
da imamo pri zanki, ko gre "v" po vseh u-jevih naslednikih,
res opravka le z u-jevimi nasledniki.  Ce bi imeli graf
predstavljen z matriko sosednosti, bi imeli tu vsakic opravka
z vsemi tockami grafa in bi morali sele preverjati, katere
so res nasledniki.  Zato bi v tem primeru ze samo za to
preverjanje porabili pri vsakem u-ju O(n) casa, skupna
zahtevnost celega postopka pa bi s tem postala
O((n + |E|) log n + n^2) -- ce je graf gost, je |E| = O(n^2)
in nismo zaradi tega nic na slabsem, ce je graf redek (recimo,
da je |E| = O(n)), pa smo na slabsem, saj dobimo O(n^2) namesto
O(n log n).  To je le se en dokaz, da se predstavitev grafa
z matriko sosednosti bolj splaca pri gostih grafih, pri
redkih pa lahko prihranimo cas, ce imamo predstavitev z seznami
sosedov.

Skratka, Dijkstrov algoritem pride prav, ce iscemo najkrajse
poti v grafih, pa graf ni dovolj lep, da bi lahko uporabili
topolosko urejanje (zahteva usmerjen graf brez ciklov) ali
pa iskanje v sirino (zahteva konstantno dolzino povezav).

Tule je nekaj nalog, kjer pride prav Dijkstrov algoritem:
- http://acm.uva.es/p/v3/341.html
- http://acm.uva.es/p/v3/393.html
- http://acm.uva.es/p/v4/423.html
- http://acm.uva.es/p/v5/589.html

Kot se pogosto zgodi pri nalogah z grafi, mora biti clovek
vcasih najprej malo zvit, da v nalogi sploh opazi primeren
graf.

Mimogrede, ko smo ze ravno pri grafih -- vcasih graf, ki ga
dobimo, ni aciklicen, vendar avtorji naloge nekako skozi zobe
priznajo, da lahko iz njega zelo preprosto dobimo aciklicen
graf.  Nekaj podobnega se je zgodilo na IOI 2001 (naloga score),
v povezavi z najkrajsimi potmi pa tudi na CERC 1998
(naloga http://acm.uva.es/p/v7/721.html -- na tekmovanju
so se potrudili dati tako casovno omejitev, da jo Dijkstra
skoraj gotovo prekoraci, topolosko urejanje pa je seveda
brez tezav dovolj hitro).

--

Bellman-Fordov algoritem za najkrajse poti v grafu

http://www.ece.nwu.edu/~guanghui/Transportation/spt/section3_2.html
http://www.owlnet.rice.edu/~comp314/02spring/lec/week5/Bellman-Ford_files/fr
ame.htm

Dijkstra je predpostavil, da v grafu nobena povezava nima
negativne dolzine (krajse bomo takim rekli "negativne
povezave").  Brez tega namrec ne drzi tisto, da je
za tocko u iz B z najmanjso d'[u] ta najkrajsa doslej
znana pot tudi dejansko najkrajsa, ravno na to pa se je
Dijkstrov algoritem oprl, da je vedel, katero tocko naslednjo
vzeti iz mnozice B.  Pri negativnih povezavah se nam vcasih
namrec splaca iti najprej dalec okoli ovinkov, ce bomo na
koncu mogoce s kaksno debelo negativno povezavo dobili
odlicno in zelo kratko pot.

Bellman-Fordov algoritem deluje tudi za grafe z negativnimi
povezavami, je pa v splosnem pocasnejsi.  Lahko si ga
predstavljamo kot dinamicno programiranje, kjer si postavimo
naslednjo obliko podproblemov:
  naj bo f(k, u) dolzina najkrajse poti od s do u z najvec
k koraki (se pravi: najvec k povezavami).

Potem je ocitno f(0, s) = 0 (od s do s pridemo v 0 korakih
preprosto tako, da smo pri miru, dolzina te poti pa je 0)
ter f(0, u) = neskoncno za u <> s (ker od s do drugih tock
ne moremo priti v 0 korakih).

Kaj pa f(k+1, u)?  Kot smo ugotavljali ze pri Dijkstrovem
algoritmu, mora biti najkrajsa taka pot (k+1 ali manj korakov
od s do u) podaljsek neke najkrajse poti v k ali manj korakih
od s do neke v, ki ji sledi se povezava od v do u.  Torej je
f(k+1, u) = minimum { f(k, v) + d[v, u] } po vseh v-jih,
za katere obstaja povezava v->u (torej po vseh u-jevih
predhodnikih).  Ce si tudi zapomnimo, pri katerem v smo
dosegli minimum, bomo dobili tudi u-jevega predhodnika na
tej najkrajsi poti.

Kot je pri dinamicnem programiranju pogosto, nam tudi tu,
ko racunamo vrednosti f(k+1, u), ni treba hraniti vrednosti
f(k-1, u), f(k-2, u) in tako naprej, ampak samo se f(k, u).

Vrednosti, ki nas dejansko zanimajo, so najkrajse poti sploh.
Te bomo nasli pri k = n-1, saj v grafu z n tockami nobena
pot ne more imeti vec kot n-1 povezav, ne da bi vsebovala
nek cikel.

Pri ciklih pa je tako: ker imamo v grafu negativne
povezave, lahko mogoce obstaja tudi kak cikel, v katerem je
vsota dolzin povezav negativna.  V tem primeru lahko, ce se
le da doseci ta cikel, po njem potujemo znova in znova in
nam skupna dolzina poti le pada.  Zato tedaj najkrajsa pot
od s do u, ce je mogoce od s do u priti skozi kak tak cikel,
sploh ne obstaja, ker lahko sestavimo poljubno kratke poti,
ce ponavljamo tisti cikel.

Prisotnost negativnih ciklov bi lahko racunali na primer tako,
da bi izracunali vrednosti f(k, u) se za k = n in s tem dali
nasim potem dovolj casa, da izrabijo vsaj en obhod po negativnih
ciklih, ce so jih sposobne doseci.  Ce se torej f(n-1, u) pri
kaksnem u razlikuje od f(n, u), potem vemo, da obstajajo v grafu
negativni cikli.  O tem se lahko prepricamo tudi z naslednjim
razmislekom.
- Ce za nek u velja f(n, u) < f(n-1, u), potem obstaja neka pot P
  od s do n po n korakih, ki je krajsa od vsake poti od s do u
  po n-1 korakih; toda pot na n korakih gotovo vsebuje nek cikel;
  ce tega cikla ne obiscemo, dobimo neko pot P' od s do u na n-1
  ali manj korakih (natancneje povedano, P' ima toliko manj
  korakov kot P, kolikor je povezav na ciklu); ce je dolzina
  cikla nenegativna, bo ta P' tako dolga kot P; toda mi smo
  rekli, da je P krajsa, torej mora biti cikel negativen.
- Zdaj pa razmislimo se v obratno smer: recimo, da obstaja nek
  negativen cikel, npr. u[0]->u[1]->...->u[p-1]->u[p] in u[p]=u[0];
  pa recimo, da bi vendarle bilo f(n-1, u[i]) = f(n, u[i]) za
  vse u[i] na tem ciklu; no, za vsako tocko na ciklu velja, da
  bo najkrajsa pot do nje v n korakih najvec tako dolga kot
  pot, ki jo dobimo, ce vzamemo najkrajso pot v n-1 korakih do
  njene predhodnice na ciklu, nato pa naredimo iz te predhodnice
  se korak v naso tocko: torej
    f(n, u[i]) <= f(n-1, u[i-1]) + d(u[i-1], u[i]);
  sestejmo te neenakosti po vseh tockah v ciklu:
    f(n, u[1]) + ... + f(n, u[p-1]) + f(n, u[p]) <=
      <= f(n-1, u[0]) + f(n-1, u[1]) + ... + f(n, u[p-1]) +
       + d(u[0], u[1]) + d(u[1], u[2]) + ... + d(u[p-1], u[p]);
  spomnimo se, da imamo cikel in je u[0] ista tocka kot u[p]:
    f(n, u[1]) + ... + f(n, u[p-1] + f(n, u[0]) <=
      <= f(n-1, u[0]) + f(n-1, u[1]) + ... + f(n, u[p-1]) +
       + d(u[0], u[1]) + d(u[1], u[2]) + ... + d(u[p-1], u[p]);
  spomnimo se, da smo predpostavili, da je f(n-1, u[i]) = f(n, u[i])
  za vse u[i] na tem ciklu; zato sta vsoti
     f(n, u[1]) + ... + f(n, u[p-1] + f(n, u[0])
  in f(n-1, u[0]) + f(n-1, u[1]) + ... + f(n, u[p-1]) enaki.
  Ostane torej
    0 <= d(u[0], u[1]) + d(u[1], u[2]) + ... + d(u[p-1], u[p]),
  ta neenacba pa trdi, da je vsota dolzin povezav na ciklu
  vecja ali enaka 0.  To pa je v protislovju s predpostavko, da je
  opazovani cikel negativen.
Ta razmislek je torej pokazal, da obstajajo v grafu negativni
cikli natanko takrat, kadar je f(n, u) < f(n-1, u) za kaksno tocko u.
[No, v resnici velja to le za negativne cikle, ki so dosegljivi
iz zacetne tocke s.  Ce nek cikel ni dosegljiv iz s, so za
tocke na njem vse vrednosti f(karkoli, u) itak enake neskoncno,
saj poti od s do teh tock sploh ni.]

Postopek bi lahko zapisali takole:
(to, da neka tocka iz s sploh ni dosegljiva v k korakih,
bomo tule zapisali ne z f[k, u] = neskoncno, kar bi bilo
v praksi lahko nerodno, pac pa s p[k, u] = -1, drugace pa
bo p[k, u] vsebovala u-jevega predhodnika na tej poti).

var f, p: array[0..n-1, 1..n] of Integer;
for u := 1 to n do p[0, u] := -1;
f[0, s] := 0; p[0, s] := s;
for k := 1 to n-1 do
  for u := 1 to n do
    begin
    (* ena kandidatka za najkrajso pot v najvec k korakih
       je seveda tudi najkrajsa pot v najvec k-1 korakih *)
    p[k, u] := p[k-1, u]; f[k, u] := f[k-1, u];
    for v := [po u-jevih predhodnikih] do
      if p[k-1, v] >= 0 then
        (* vemo, da obstaja pot od s do v z k-1 ali manj koraki *)
        if p[k, u] = -1 or f[k-1, v] + d[v, u] < f[k, v] then
          (* pot prek v je nova najkrajsa znana pot (ali pa
             sploh prva znana pot) od s od u z najvec k koraki *)
          begin p[k, u] := v; f[k-1, u] := f[k-1, v] + d[v, u]; end;
    end;

Na koncu imamo v f[n-1, u] dolzine iskanih najkrajsih poti,
v p[n-1, u] pa prednike posameznih tock na teh poteh.
Ce pa je p[n-1, u] = -1, pa tocka u sploh ni dosegljiva iz s.

Kot se pri dinamicnem programiranju pogosto zgodi, v resnici
ni treba vec hraniti vrednosti f[k-1, 1..n], ko racunamo
f[k+1, 1..n] -- takrat potrebujemo le se f[k, 1..n].

Casovna zahtevnost: ocitno je treba iti n-krat skozi vse tocke
in vse povezave.  Torej O(n * (n + |E|)), ce imamo sezname sosedov,
oz. O(n * (n + n^2)) = O(n^3), ce imamo matriko sosedov.

Malo bolj "slampasta" razlicica Bellman-Fordovega algoritma
bi bila lahko tudi taksna:

var f, p: array[1..n] of Integer;
for u := 1 to n do p[u] := -1;
f[s] := 0; p[0, s] := s;
for k := 1 to n-1 do
  (* pojdimo po vseh povezavah (v->u) nasega grafa *)
  for u := 1 to n do for v := [po u-jevih predhodnikih] do
    (* poglejmo, ce je mogoce sestaviti novo najkrajso
       pot do u-ja, ki v zadnjem koraku uporabi povezavo v->u *)
    if p[v] >= 0 then
      if p[u] = -1 or f[v] + d[u, v] < f[u] then
        begin p[u] := v; f[u] := f[v] + d[u, v]; end;
(* na tem mestu lahko odkrijemo negativne cikle tako, da
   izvedemo se eno iteracijo gornje zanke in gledamo, ce se
   kaksna f[u] se spremeni *)
for u := 1 to n do for v := [po u-jevih predhodnikih] do
  if p[v] >= 0 then
    if p[u] = -1 or f[v] + d[u, v] < f[u] then
      WriteLn('Obstaja negativen cikel.');

Tu torej v f[u] preprosto hranimo dolzino najkrajse doslej znane
poti od s do u, v p[u] pa u-jevega predhodnika na tej poti.
Potem gremo n-krat skozi ves graf in pri vsaki tocki u za
vse njene predhodnice v pogledamo, ce bi bila najkrajsa pot
od s do v, podaljsana s povezavo v->u, mogoce nova najkrajsa
pot od s do u.  Razlika je v tem, da bi prejsnji algoritem
vedno uporabljal vrednosti f[k-1, v], tale pa ima le eno tabelo
in pac uporablja vrednost f[v], ki smo jo mogoce ze popravili
pri trenutnem k-ju.  Zato pri dolocenem k v resnici ne
odkrivamo nujno samo poti dolzine najvec k, ampak lahko
odkrijemo tudi ze daljse poti.

--

Algoritmi za najkrajse poti med vsemi pari tock

Doslej smo videli algoritme, ki nam poiscejo najkrajse poti od
izbrane tocke s do vseh tock v grafu (oz. do vseh, ki so pac
dosegljive iz s).  Kaj pa, ce bi si zeleli za vsak par tock
najti najkrajso pot med njima?

Ocitna moznost je, da vzamemo kak algoritem za najkrajse poti
od s do ostalih, pa okoli tega ovijemo se eno for zanko, ki
bo peljala spremenljivko s prek vseh vozlisc v grafu.
Casovna zahtevnost se nam s tem preprosto pomnozi za faktor n
(ce je n stevilo tock v grafu).

Ce to naredimo z Bellman-Fordom, dobimo O(n^2 (n + |E|)) (pri
seznamih sosedov) oz. O(n^4) (pri matriki sosednosti).  No, ce
je graf gost, je |E| = O(n^2) in imamo v obeh primerih O(n^4).
Kakorkoli ze, izkaze se, da lahko v tem primeru naredimo tudi
ucinkovitejso razlicico Bellman-Forda:

Imejmo zdaj neko tabelo f[0..n-1, 1..n, 1..n], v kateri
vrednost f[k, u, v] pove dolzino najkrajse poti od u do v
z najvec k koraki.  Potem, ce poznamo vrednosti f[k1, u, v]
in f[k2, u, v] za vse u in v, lahko preprosto pridemo do
vrednosti f[k1+k2, u, v].  Najkrajsa pot od u do v
z najvec k1+k2 koraki je pac sestavljena iz neke najkrajse
poti od u do nekega w z najvec k1 koraki in nato iz neke
najkrajse poti od tega w do nasega v z najvec k2 koraki.
Vse, kar je treba, je poiskati w, ki bo dal najkrajso
skupno pot.

(* Tale podprogram napolni tabelo f[k1+k2, u, v]
   ob predpostavki, da sta f[k1, u, v] in f[k2, u, v]
   ze pripravljeni. *)
procedure Izracun(k1, k2: Integer);
var u, v, w: Integer; d: Double;
begin
  for u := 1 to n do for v := 1 to n do
    begin
    d := neskoncno;
    for w := 1 to n do
      if f[k1, u, w] + f[k2, w, v] < d then
        d := f[k1, u, w] + f[k2, w, v];
    f[k1 + k2, u, v] := d;
    end;
end;

Zdaj lahko zacnemo takole: za k = 1 ni tezko pripraviti
tabele f[1, u, v] -- se je u = v, imamo 0, ce obstaja
povezava u->v, imamo dolzino te povezave, drugod pa imejmo
vrednost neskoncno kot znak, da take poti sploh ni.

Potem lahko, ce poklicemo Izracun(1, 1), dobimo f[k, u, v]
za k = 2.  Nato lahko z Izracun(2, 2) izracunamo za k = 4.
Od tam za k = 8, nato za k = 16 in tako naprej.
Nas pa bo na koncu zanimal k = n-1.  Ocitno lahko n-1
izrazimo kot vsoto potenc stevila 2 -- kot pri dvojiskem
zapisu.  Na primer: pri n = 30 imamo 29 = 16 + 8 + 4 + 1,
torej moramo poklicati Izracun(1, 4) --> dobimo za k = 5;
Izracun(5, 8) --> dobimo za k = 13; Izracun(13, 16) --> dobimo
za k = 29.  Ce je 2^b najvisja potenca stevila 2, ki jo
potrebujemo pri stevilu n-1, bomo morali Izracun klicati b-krat,
da pridemo do vrednosti f[2^j, u, v] za j od 1 do b,
nato pa bo treba Izracun klicati se najvec b-krat, da se nam
te potence stevila 2 sestejejo in dobimo f[n-1, u, v].

Negativne cikle lahko tu opazimo po negativnih stevilkah na
diagonali -- ce se da od u do u priti po kaksni poti z negativno
dolzino, mora biti iz u-ja dosegljiv kak negativni cikel.
Pravzaprav je lahko cikel dolg tudi n korakov, zato bi bilo
za taksno preverjanje bolje izracunati se f[n, u, v].

Lepota gornjega postopka je seveda v tem, da traja vsak
klic podprograma Izracun le O(n^3) casa, klicev pa je, kot
smo pravkar videli, O(b) = O(log n).  Celoten postopek torej
traja O(n^3 log n) casa.  (Tudi s pomnilnikom lahko varcujemo,
ce saj tabele za k = 2^j ni treba vec hraniti, ko enkrat
izracunamo tabelo za k = 2 * 2^j in ko jo po potrebi tudi
upostevamo v tabeli, kjer se nam nabira vrednost k = n-1.)
Skratka, O(n^3 log n) je lahko bolje kot O(n^2 (n + |E|)), ce
graf ni prevec redek.

--

Floyd-Warshallov algoritem

Tudi ta temelji na dinamicnem programiranju, le malo drugace
si zastavi podprobleme.  Tukaj si recemo: naj bo f[u, v, k]
dolzina najkrajse poti od u do v, ki nikoli ne obisce tock
k+1, k+2, ..., n (razen mogoce u in v same).  Mislimo si, da
so tocke ostevilcene od 1 do n.

Ocitno je f[u, v, 0] = d[u, v], ce med u in v ne obstaja
neposredna povezava, drugace pa je f[u, v, 0] = neskoncno,
saj nam k = 0 prepoveduje, da bi med u in v obiskali se kaksno
drugo tocko.

Kaj pa f[u, v, k+1]?  Najkrajsa pot od u do v, ki sme vmes
obiskovati tudi tocke od 1 do k+1, ima dve moznosti: ali tocke
k+1 sploh ne obisce (v tem primeru je to isto kot f[u, v, k])
ali pa jo obisce -- v tem primeru pa je sestavljena iz enega
dela, ki gre od u do k+1 (in vmes obiskuje le tocke 1..k),
in enega ki gre od k+1 do v; tu je torej f[u, v, k+1] =
f[u, k+1, k] + f[k+1, v, k].  Na koncu bomo seveda obdrzali
tisto moznost, ki je krajsa.

for u := 1 to n do for v := 1 to n do
  f[u, v, 0] := d[u, v];
for k := 1 to n do
  for u := 1 to n do for v := 1 to n do
    f[u, v, k] := min(f[u, v, k-1], f[u, k, k-1] + f[k, v, k-1]);

Na koncu imamo v f[u, v, n] najkrajse poti, ki smejo obiskovati
katerekoli tocke, torej so to najkrajse poti sploh.  Negativne
cikle bi se dalo verjetno tudi tu prepoznati po f[u, u, n] < 0.
Le dejanski potek poti bi bilo tule malo tezje dobiti (ampak saj
ga ne potrebujemo vedno).  Verjetno bi si lahko zapomnili, katero
od obeh moznosti smo izbrali pri racunanju minimuma, ko smo
racunali f[u, v, k], in iz tega dobili podatek, ali najkrajsa
pot f[u, v, k] obisce tocko k ali ne; ce jo, bi sli po enakem
postopku sestavljat potek poti f[u, k, k-1] in f[k, v, k-1],
drugace pa bi sli gledat potek poti f[u, v, k-1].

Lepo pri opisanem algoritmu pa je, da pozre O(n^3) casa,
ce imamo n tock v grafu (tri gnezdene zanke od 1 do n).
To je sicer se vedno slabse kot n-krat ponovljen Dijkstra, ce
je graf redek.  Ce pa je gost ali pa vsebuje negativne povezave,
je Floyd-Warshall ze lahko boljsi.  Ce drugega ne, je tudi
zelo enostaven.  Slabost v primerjavi z Bellman-Fordom bi
bila lahko npr. ta, da pri Floyd-Warshallu tezko pridemo do
poti, sestavljenih iz konkretnega stevila korakov.

--

Ce povzamemo, kar zdaj vemo o algoritmih za najkrajse
poti v grafih (naj bo |V| stevilo tock v grafu, |E| pa stevilo
povezav).

algoritem                      omejitve            cas. zahtevnost

- za najkrajse poti od ene tocke do ostalih:

topolosko urejanje     usmerjen aciklicen graf      O(|V| + |E|)
BFS                    vse povezave enako dolge     O(|V| + |E|)
Dijkstra             ni negativnih povezav      O((|V| + |E|) lg |V|)
Bellman-Ford                                    O((|V| + |E|) |V|)

- za najkrajse poti med vsemi pari tock:

topolosko urejanje ali BFS
  iz vsake tocke posebej     enake kot zgoraj   O((|V| + |E|) |V|)
Dijkstra iz vsake tocke posebej   ni neg. pov.  O((|V| + |E|) |V| lg |V|)
Bellman-Ford iz vsake tocke posebej             O((|V| + |E|) |V|^2)
Bellman-Ford z matriko                             O(|V|^3 lg |V|)
Floyd-Warshall                                        O(|V|^3)

--

Ce vas Bellman-Ford ali Floyd-Warshall prevec begata, razumete
pa dinamicno programiranje, lahko nanju preprosto pozabite, pa
v potrebi razmisljate z dinamicnim programiranjem -- cisto mogoce
je, da boste sami prisli do ekvivalentnega pristopa.

Po mojih opazanjih je za razna tekmovanja koristno poznati topolosko
urejanje in iskanje v sirino, obcasno tudi Dijkstro, drugi pa so ze
malo bolj eksoticni.

Nekaj ACMovih nalog, kjer lahko uporabimo Bellman-Forda:

- http://acm.uva.es/p/v1/104.html
- http://acm.uva.es/p/v4/436.html
- http://acm.uva.es/p/v5/558.html